#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <iostream>
#include <vector>
#include <sstream>
#include <fstream>
#include <limits.h>
#include <queue>
#include <list>
#include <pthread.h>
#include <sys/time.h>
#include "constants.h"

typedef unsigned char uchar;
typedef unsigned int uint;
typedef unsigned short ushort;
typedef struct {
	double sentTime;
	int filePosition;
	uint sequenceNumber;
} packetInfo;

using namespace std;

int sockfd, rv, numbytes, timeOut, numRoundTrips;
bool timeoutPingCalculated;
uint LAR = UINT_MAX;
uint LFS = 0;
uint SWS;
list<uint> listOfAcks;
struct addrinfo hints, *servinfo, *p;
struct sockaddr_storage their_addr;
struct timeval tv;
struct timezone tz;
socklen_t addr_len;
pthread_mutex_t print_mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t LAR_mutex = PTHREAD_MUTEX_INITIALIZER;
fstream fin;
priority_queue<uint,deque<uint,allocator<uint> >,greater<uint> > packetsLost;
priority_queue<uint,deque<uint,allocator<uint> >,greater<uint> > packetsDmg;
double firstTime;

/** bindSocket - int
  * 	Sets up the socket to use the unreliable data transfer, rather than the 
  *		normal sockets.
  *
  * string hostName - The hostname that we wish to send our data to.
  */
int bindSocket(string hostName){
	memset(&hints, 0, sizeof hints);
	hints.ai_family = AF_UNSPEC;
	hints.ai_socktype = SOCK_DGRAM;
	
// get the info needed to set up the socket	
	if ((rv = getaddrinfo(hostName.c_str(), SERVERPORT, &hints, &servinfo)) != 0) {
		fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rv));
		return 1;
	}
		
		for(p = servinfo; p != NULL; p = p->ai_next) {
		if ((sockfd = socket(p->ai_family, p->ai_socktype,
				p->ai_protocol)) == -1) {
			perror("talker: socket");
			continue;
		}
		break;
	}
		
	if (p == NULL) {
		fprintf(stderr, "talker: failed to bind socket\n");
		return 2;
	}
		return 0;
}

/**
  * setSequesnceNumber - void
  * 	Sets up the header of the packet array to include the sequence number.
  *		Since we wanted the sequence number we wanted to support an unsigned int,
  * 	We needed 4 char to cover the range of int, since an int is typically 4 
  *		bytes, and a char is 1 byte.
  *	uchar* data - The packet, with all information
  * uint sequenceNumber - The number that will be inserted into the array header.
  */
void setSequenceNumber(uchar* data, uint sequenceNumber){
	data[0] = (sequenceNumber & 0x000000FF);
	data[1] = ((sequenceNumber & 0x0000FF00) >> 8);
	data[2] = ((sequenceNumber & 0x00FF0000) >> 16);
	data[3] = ((sequenceNumber & 0xFF000000) >> 24);
}

/**
  * getSeqNum - uint
  *		Retrieves the sequence number from the header of the array. This 
  *		is essentially the reverse of the above function, and will pull
  *		4 one byte chars out, and add them together.
  * uchar* data - The packet, with all information.
  */
uint getSeqNum(uchar* data){
	uint seq = (data[3] << 24) + (data[2] << 16) + (data[1] << 8) + data[0];
	return seq;
}

/**
  * setCheckSum - void
  * 	This method inserts the pre-calculated checksum into the packet 
  *		array.
  * uchar* data - The packet array.
  * ushort checksum - The checksum of the given data.
  */
void setCheckSum(uchar* data, ushort checksum){
	data[4] = (checksum & 0x00FF); 
	data[5] = ((checksum & 0xFF00) >> 8);
}

/**
  * setBOF - void
  *		A method to toggle an area in the packet to signify if the
  *		following data is the beginning of the stream/file/data.
  * uchar* data - The Packet array to have the bit flipped.
  */
void setBOF(uchar* data){
	printf("%X\n",data[6]);
	data[6] = SIG_BOF;
}

/**
  * setEOF - void
  *		A method to toggle the end of file bit, allowing for the
  * 	reciever to know when to terminate the file.
  * uchar* data - The Packet array to have the bit flipped.
  */
void setEOF(uchar* data){
	data[6] = SIG_EOF;
}

/**
  * setPING - void
  *		A method to let the reciever know if the current packet is
  *		being used as a ping timer packet.
  * uchar* data - The packeet array to have the bit flipped.
  */
void setPING(uchar* data){
	data[6] = SIG_PING;
}

/**
  * isPING - bool
  *		A method to see if the packet is a PING packet. This is needed
  *		on the sender to see if the result and end timers.
  * uchar* data - The packet array.
  */
bool isPING(uchar* data){
	bool result = false;
	if(data[4] == SIG_PING){
		result = true;
	}
	return result;
}

/**
  * cksum - ushort
  *		A method to determine what the checksum is for a given message,
  *		and return the value.
  *	uchar* addr - the data to have the checksum calculated on.
  * int count   - The total 
  */
ushort cksum(uchar* addr, int count) {
	/* Compute Internet Checksum for "count" bytes
	*         beginning at location "addr".
	*/
	register long sum = 0;
	register long add = 0;
	ushort* shrt_addr = (ushort*)addr;
	while( count > 1 )  {
		/*  This is the inner loop */
	   sum += *shrt_addr++;
	   count -= 2;
	}
	
	/*  Add left-over byte, if any */
	if( count > 0 ){
		sum += *addr;
	}

	/*  Fold 32-bit sum to 16 bits */
	while (sum>>16)
		sum = (sum & 0xffff) + (sum >> 16);
	add = sum;
	sum = ~sum;
	return sum;
}

/**
  * printdata - void
  * 	Prints the packet array.
  * uchar* data - the packet array
  * int count   - the limit of the array to be calculated.
  */
void printdata(uchar* data, int count){
	cout << " | Data: ";
	for(int i = 0; i < count;i++){
		printf("[%d]: 0x%X ",i,data[i]);
	}
	cout << " |" << endl;
}

/**
  * receiveAcks - void
  * 	This method is threaded to allow simultaneous transmission and
  *		recieving of data packets. This specific method should be spawned
  *		as threads and therefore, must be carefully planned when methods
  *		are called. This focuses on acknowledge statements, and allows the
  *		sliding window to move while data is being sent. This will be 
  *		noticably useful for Selective Repeat and Go-back In
  * void * blah - not used, however, required for pthreads.
  */
void *receiveAcks(void *blah){
	/*
	in the recvfrom thread, check if the packet you are getting back is a ping
and if so, print something different out
and set a variable
something you can wait for after sending the ping to time it
*/
	while(1){
		uchar ack[5];
		int seqNum;
		addr_len = sizeof their_addr;
		uchar isPingChar;
		if ((numbytes = recvfrom(sockfd, ack, 5, 0,(struct sockaddr *)&their_addr, &addr_len)) == -1) {
				perror("recvfrom");
				exit(1);
		} else {
			isPingChar = ack[4];
			pthread_mutex_lock( &print_mutex );
			pthread_mutex_lock( &LAR_mutex );
			seqNum = getSeqNum(ack);

			if (ack[4]==0xFC){
				cout<<" Received ping "<<endl;
					gettimeofday(&tv, &tz);
				double newCurrentTime = tv.tv_sec + (tv.tv_usec * .000001);
				cout<<"Time after: "<<newCurrentTime<<endl;	//Store time of day in tv struct.
				double roundTripTime = (newCurrentTime - firstTime)*1000;
				cout<<"Ping Time: "<< roundTripTime<<endl;
				timeOut = roundTripTime * numRoundTrips;
			}else{
				cout << "Received ACK for " << seqNum << endl;
			}
			LAR = seqNum;
			listOfAcks.push_back(seqNum);
			pthread_mutex_unlock( &LAR_mutex );
			pthread_mutex_unlock( &print_mutex );

		}
	}
	return (void*)0;
}

/**
  * sendBlankPacket - int
  *		This method sends a blank packet, with only header info filled out.
  *		The blank packet is used for the PING timer, since there is no data
  *		that needs to be transmitted.
  * int packetSize - Needs to be filled, since the this variable could change.
  */
int sendBlankPacket(int packetSize){
	uint sequenceNumber=0;
	uchar data[packetSize];
	memset(data,0,packetSize);
	setSequenceNumber(data,sequenceNumber);
	setPING(data);
	setCheckSum(data, cksum(data,packetSize));
	gettimeofday(&tv, &tz);	//Store time of day in tv struct.
	 firstTime = tv.tv_sec + (tv.tv_usec * .000001);
	cout<<"Time before "<< firstTime<<endl;
	
	if ((numbytes = sendto(sockfd, data,packetSize, 0,p->ai_addr, p->ai_addrlen)) == -1) {
		perror("talker: sendto");
		exit(1);
	}
	cout << "Packet "<<sequenceNumber<<" sent (" << numbytes << " bytes) "<< endl;
//	gettimeofday(&tv, &tz);
//	double newCurrentTime = tv.tv_sec + (tv.tv_usec * .000001);
//	cout<<"Time after: "<<newCurrentTime<<endl;	//Store time of day in tv struct.
//	cout<<"Ping Time: "<<newCurrentTime - firstTime<<endl;
	return 0;
}

/**
  * stopAndWait - int
  * 	The stop and wait protocol. This handles how the data and acknowledgements
  * 	should be sent and recieved. Stop and wait handles one packet at a time,
  *		and waits for the single acknowledgement before continuing with transmission.
  *		This is the simplest protocol, and will be selected as the default, if no
  *		protocol was chosen. The method will return 0 if it was successful.
  * int packetSize - Since the user can enter a packetsize, this must be taken into 
  *					 consideration.
  * string filepath - The path to the file which will be sent.
  */
int stopAndWait(int packetSize, string filePath){
	uchar data[packetSize];
	memset(data,0,packetSize);
	int currentPacketSize;
	int arraySize;
	if (packetSize > MAX_PACKET_SIZE){
		arraySize = MAX_PACKET_SIZE;
	} else {
		arraySize = packetSize;
	}
	int i;
	char tmp;
	int leftover;
	uint sequenceNumber = 0;
	bool lost = false;
	bool sendPacket = false;
	bool resend = false;
	packetInfo window;
	//Initialize window.
	window.sequenceNumber = 0;	 
	window.sentTime = 0;
	window.filePosition = 0;
	
	while(fin.good()){
		if(sequenceNumber == (LAR + 1) || sequenceNumber == 0){
			sendPacket = true;
		} else {
			sendPacket = false;
		}
		gettimeofday(&tv, &tz);	//Store time of day in tv struct.
		double currentTime = tv.tv_sec + (tv.tv_usec * .000001);
		double difference = ((currentTime - window.sentTime) * 1000);
		if(difference > timeOut && sequenceNumber != 0){
			cout << "Packet " << window.sequenceNumber << " timed out." << endl;
			if(sequenceNumber > 0){
				sequenceNumber--;
			}
			resend = true;
		}
		if(sendPacket || resend){
			leftover = packetSize - HEADER_SIZE;
			currentPacketSize=arraySize;
			setSequenceNumber(data,sequenceNumber);
			window.sequenceNumber = sequenceNumber;
			if(sequenceNumber == packetsLost.top() && packetsLost.size() > 0) {
				lost = true;
				cout << "Losing packet " << sequenceNumber << endl;
				packetsLost.pop();
			} else {
				lost = false;
			}
			while (leftover > 0){
				if(sequenceNumber == 0){
					//We need to send the file name for the server.
					setBOF(data);
					for(i = 0;i<(int)filePath.length();i++){
						data[i + HEADER_SIZE] = filePath[i];
					}
					i = i + HEADER_SIZE;
					leftover = 0;
				} else {
					if(resend){
						fin.seekg(window.filePosition);
					} else {
						window.filePosition = fin.tellg();
					}
					for(i = HEADER_SIZE; i < currentPacketSize && !fin.eof(); i++){
						leftover--;
						tmp = fin.get();
						if(!fin.eof()){
								data[i] = tmp;
						} else {
								i--;
								leftover = 0;
								setEOF(data);
						}
					}
				}
				setCheckSum(data,cksum(data,i));
				if(sequenceNumber == packetsDmg.top() && packetsDmg.size()>0){
					cout << "Damaging packet " << sequenceNumber << endl;
					data[10] = data[10] + 1;	//Damage the packet randomly...
					packetsDmg.pop();
				}
				currentPacketSize = leftover;
				if (!lost){
					if ((numbytes = sendto(sockfd, data, i, 0,p->ai_addr, p->ai_addrlen)) == -1) {
							perror("talker: sendto");
							exit(1);
					}
				}
				LFS = sequenceNumber;
				gettimeofday(&tv, &tz);	//Store time of day in tv struct.
				window.sentTime = tv.tv_sec + (tv.tv_usec * .000001);	//Store milliseconds in our window struct
				pthread_mutex_lock( &print_mutex );
				cout << "Packet " << sequenceNumber << " sent (" << numbytes << " bytes) Total read: " << i << endl;
				pthread_mutex_unlock( &print_mutex );
				resend = false;
				memset(data,0,packetSize);
			}
			sequenceNumber++;
		}
	}
	while(LFS != LAR){}
	return 0;
}

/**
  * goBackN - int
  * 	The stop and wait protocol. This handles how the data and acknowledgements
  * 	should be sent and recieved. Stop and wait handles one packet at a time,
  *		and waits for the single acknowledgement before continuing with transmission.
  *		This is the simplest protocol, and will be selected as the default, if no
  *		protocol was chosen. The method will return 0 if it was successful.
  * int packetSize - Since the user can enter a packetsize, this must be taken into 
  *					 consideration.
  * string filepath - The path to the file which will be sent.
  */
int goBackN(int packetSize, string filePath, uint windowSize)
{
	uchar data[packetSize];
	memset(data,0,packetSize);
	int currentPacketSize;
	int arraySize;
	if (packetSize > MAX_PACKET_SIZE){
		arraySize = MAX_PACKET_SIZE;
	} else {
		arraySize = packetSize;
	}
	packetInfo sentPackets[windowSize]; //which sequence numbers we have sent.
	bool resendPackets[windowSize];
	memset( sentPackets, NULL, windowSize );
	memset( resendPackets, false, windowSize );
	int i;
	char tmp;
	int leftover;
	uint sequenceNumber = 0;
	bool lost = false;
	packetInfo window;
	//Initialize window.
	window.sequenceNumber = 0;	 
	window.sentTime = 0;
	window.filePosition = 0;
	
	while(fin.good())
	{
		i = 0;
		//check all the timeouts and set any to resend if needed.
		while ((i < windowSize)  && (sentPackets[i] != NULL))
		{
			gettimeofday(&tv, &tz);	//Store time of day in tv struct.
			double currentTime = tv.tv_sec + (tv.tv_usec * .000001);
			double difference = ((currentTime - sentPackets[i].sentTime) * 1000);
			if(difference > timeOut && sentPackets[i].sequenceNumber != 0)
			{
				cout << "Packet " << sentPackets[i].sequenceNumber << " timed out." << endl;
				resendPacket[i] = true;
			}
			i++;
		}
		for( int j =0; j < windowSize; j++)
		{ 
			leftover = packetSize - HEADER_SIZE;
			currentPacketSize=arraySize;
			setSequenceNumber(data,sequenceNumber);
			window.sequenceNumber = sequenceNumber;
			if(window.sequenceNumber == packetsLost.top() && packetsLost.size() > 0) 
			{
				lost = true;
				cout << "Losing packet " << sequenceNumber << endl;
				packetsLost.pop();
			} else {
				lost = false;
			}
			while (leftover > 0)
			{
				if(window.sequenceNumber == 0)
				{
					//We need to send the file name for the server.
					setBOF(data);
					for(i = 0;i<(int)filePath.length();i++)
					{
						data[i + HEADER_SIZE] = filePath[i];
					}
					i = i + HEADER_SIZE;
					leftover = 0;
				} else {
					if(resendPackets[j])
					{
						fin.seekg(sentPackets[j].filePosition);
					} else {
						window.filePosition = fin.tellg();
					}
					for(i = HEADER_SIZE; i < currentPacketSize && !fin.eof(); i++)
					{
						leftover--;
						tmp = fin.get();
						if(!fin.eof())
						{
								data[i] = tmp;
						} else {
								i--;
								leftover = 0;
								setEOF(data);
						}
					}
				}
				setCheckSum(data,cksum(data,i));
				if(window.sequenceNumber == packetsDmg.top() && packetsDmg.size()>0)
				{
					cout << "Damaging packet " << sequenceNumber << endl;
					data[10] = data[10] + 1;	//Damage the packet randomly...
					packetsDmg.pop();
				}
				currentPacketSize = leftover;
				
				if (!lost)
				{
					if ((numbytes = sendto(sockfd, data, i, 0,p->ai_addr, p->ai_addrlen)) == -1) 
					{
							perror("talker: sendto");
							exit(1);
					}
				}
				LFS = sequenceNumber;
				gettimeofday(&tv, &tz);	//Store time of day in tv struct.
				window.sentTime = tv.tv_sec + (tv.tv_usec * .000001);	//Store milliseconds in our window struct
				//save this packet to tell if it needs to be resent.
				sentPackets[j] = window;
			
				pthread_mutex_lock( &print_mutex );
				cout << "Packet " << sequenceNumber << " sent (" << numbytes << " bytes) Total read: " << i << endl;
				pthread_mutex_unlock( &print_mutex );
				memset(data,0,packetSize);
			}
			sequenceNumber++;
		}
		bool recAck = true;
		int p = 0;
		while(recAck)
		{
			list<int>::iterator it;
			pthread_mutex_lock( &LAR_mutex );
			for(it = listOfAcks.begin(); it != listOfAcks.end(); it++)
			{
				if( sentPackets[p].sequenceNumber == *it )
				{
					sentPackets[p] = NULL;
				}
			}
			pthread_mutex_unlock( &LAR_mutex );
		}
		//wait for acknowledgements to be recieved.
		//if ack for first window spot, then move window.
		//otherwise wait for timeout to resend packet.
	}
	//need a condition to check if the window is full or not. If the window is full, no sending should happen.
	//need a way to tell if ack has been recieved for a packet. If it has, remove it from the window.

	
	//essentially n number of stop and waits, where n is the windowsize.
	return 0;
}

/**
  * sendFile - int
  * 	Sendfile organizes the user's input and initiates the file read. This 
  *		sorts the inputted data, and creates reads the file incrementally. All
  *		the while packaging them into packets and sending them off.
  * int protocol    - which protocol we should be using
  * string filepath - which file we are sending.
  * int packetSize  - how big are the packets we're sending.
  * uint windowSize - The size of the window.
  */
int sendFile(int protocol, string filePath, int packetSize, uint windowSize){
	string msg;
	fin.open(filePath.c_str(), ios::in | ios::binary)
	fin.seekg(0, ios::end);
	pthread_mutex_lock( &print_mutex );
	cout << "Size: " << fin.tellg() << endl;
	pthread_mutex_unlock( &print_mutex );
	fin.seekg(0);
	int result = 0;
	if(fin == NULL) {
			cout <<"Error opening file." << endl;
			result = 1;
	} else {
		if(packetSize == 0){
				packetSize = (int)DEFAULT_PACKET_SIZE;
		}
		
		//Calculate timeout value.
		if(timeOut == 0){
			timeOut = DEFAULT_TIMEOUT;
		} else if(!timeoutPingCalculated){
		cout<<"GETTING PING TIME-----------------------"<<endl;
			int pingTime = sendBlankPacket(packetSize);
			//Send PING packet to figure it out.
			/*
			DO THIS LEE. I have a setPING thing, all you need to do is send a blank packet in a new method with that value set.
			Receiver should know what it is getting. Use my timer stuff above to find the time in milliseconds it takes. I don't 
			expect it to be more than like 10.
			*/
		}
		
		if(protocol == 1){
			result = stopAndWait(packetSize, filePath);
		} else if(protocol == 2){
			cout << "GBN not implemented." << endl;
			result = goBackN(packetSize, filePath, windowSize);
		} else {
			cout << "Selected repeat not implemented." << endl;
		}
	}
	fin.close();
	return result;
}

/**
  * main - int
  *		Default main class. Asks for user input, and initiates the
  *		transmission process.
  */
int main(void) {
		string hostName;
        string protocol;
		string filePath;
        string packetsLostStr;
        string packetSize;
        string packetsDmgStr;
		bool cont = true;
		string windowSize;
		string contStr;
		string timeoutStr;
		
		
		//cout << "Host name: ";
        //getline(cin, hostName);
		
		while(cont){
			//cout << "Select protocol: " << endl;
			//cout << "\t1 : Stop-and-wait" << endl;
			//cout << "\t2 : Go-back-N " << endl;
			//cout << "\t3 : Selective Repeat" << endl;
			//cout << "Enter number: ";
			//getline(cin, protocol);
			//cout << "File path: ";
			//getline(cin, filePath);
			//cout << "Packet Size in KB (0 for default): ";
			//getline(cin,packetSize);
			//if(atoi(protocol.c_str()) == 1){
				//windowSize = "1";
			//} else {
				//cout << "Window size: ";
				//getline(cin, windowSize);
			//}
			
			//cout << "Packets you would like lost: ";
			//getline(cin, packetsLostStr);
			//cout << "Packets you would like damaged: ";
			//getline(cin, packetsDmgStr);
			/*
			cout<<"Use Default Timeout? (y/n) ";
			getline(cin, timeoutStr);
			timeoutPingCalculated = false;
			if (timeoutStr == "n" || timeoutStr=="N"){
				cout << "Select timeout method: " << endl;
				cout << "\t1 : Milliseconds" << endl;
				cout << "\t2 : Number of round trips" << endl;
				cout << "Enter number: ";
				getline(cin, timeoutStr);
				if (atoi(timeoutStr.c_str()) == 1){
					cout << "Please specify desired time out time (in ms): ";
					getline(cin, timeoutStr);
				} else {	// calculate timeout by round trips
					cout << "Please specify number of round trips before timeout: ";
					timeoutPingCalculated = true;
					getline(cin,numRoundTrips);
				}
			}
			timeOut = atoi(timeoutStr.c_str());
			*/
			//TESTING VARIABLES
			timeOut=5000;
			
			numRoundTrips =1;
			filePath = "1.jpg";
			hostName = "andy.cs.uwec.edu";
			packetSize = "16";
			windowSize = "1";
			protocol = "1";
			packetsLostStr = "";
			packetsDmgStr = "";
			
			istringstream packetsLostSS(packetsLostStr);
			uint buf;
			while(packetsLostSS >> buf){
					packetsLost.push(buf);
			}
			
			istringstream packetsDmgSS(packetsDmgStr);
			while(packetsDmgSS >> buf){
					packetsDmg.push(buf);
			}
			bindSocket(hostName);
			pthread_t thread;
			int s = 0;
			SWS = atoi(windowSize.c_str());
			s = pthread_create(&thread, NULL, receiveAcks,(void*)&s);
			
			if(sendFile(atoi(protocol.c_str()), filePath, atoi(packetSize.c_str()) * 1024, atoi(windowSize.c_str())) > 0){
				cout << "An error occurred. File not transferred." << endl;
			} else {
				cout << "File successfully transferred." << endl;
			}
			pthread_mutex_lock( &print_mutex );
			cout << "Would you like to send another file (y/n)? ";
			pthread_mutex_unlock( &print_mutex );
			getline(cin, contStr);
			if(contStr == "n" || contStr == "N"){
				cont = false;
			}
		}
		freeaddrinfo(servinfo);
		close(sockfd);
        return 0;
};

